/**
 * TextFormat
 *
 * @author Chris Pratt
 *
 * 3/2/1999
 */
package com.anodyzed.onyx.text;

import com.anodyzed.onyx.util.Misc;
import com.anodyzed.onyx.util.PrototypeMap;

/**
 * <code>TextFormat</code> provides a means to produce concatenated
 * messages in language-neutral way. Use this to construct messages
 * displayed for end users.
 *
 * <p>
 * <code>TextFormat</code> takes a set of objects, formats them, then
 * inserts the formatted strings into the pattern at the appropriate places.
 *
 * <p>
 * <strong>Note:</strong>
 * <code>TextFormat</code> differs from the <code>java.text.Format</code>
 * classes in that you create a <code>TextFormat</code> object with one
 * of its constructors (not with a <code>getInstance</code> style factory
 * method). The factory methods aren't necessary because <code>TextFormat</code>
 * itself doesn't implement locale specific behavior. Any locale specific
 * behavior is defined by the pattern that you provide as well as the
 * sub-formats used for inserted arguments.
 *
 * <h4><a name="patterns">Patterns and Their Interpretation</a></h4>
 *
 * <code>TextFormat</code> uses patterns of the following form:
 * <blockquote><pre>
 * <i>TextFormatPattern:</i>
 *         <i>String</i>
 *         <i>TextFormatPattern</i> <i>FormatElement</i> <i>String</i>
 *
 * <i>FormatElement:</i>
 *         { <i>BeanReference</i> }
 *         { <i>BeanReference</i> , <i>FormatType</i> }
 *         { <i>BeanReference</i> , <i>FormatType</i> , <i>FormatStyle</i> }
 * 
 * <i>BeanReference:</i>
 *         { <i>ArgumentIndex</i> }
 *         { <i>ArgumentIndex</i> . <i>BeanProperty</i> }
 *         { <i>ArgumentIndex</i> . <i>BeanProperty</i> [ . <i>BeanProperty</i> ]... }
 *
 * <i>FormatType: one of </i>
 *         number date time array list map boolean
 *
 * <i>FormatStyle:</i>
 *         csv
 *         short
 *         medium
 *         long
 *         full
 *         integer
 *         currency
 *         percent
 *         duration
 *         <i>SubformatPattern</i>
 *
 * <i>String:</i>
 *         <i>StringPart<sub>opt</sub></i>
 *         <i>String</i> <i>StringPart</i>
 *
 * <i>StringPart:</i>
 *         ''
 *         ' <i>QuotedString</i> '
 *         <i>UnquotedString</i>
 *
 * <i>SubformatPattern:</i>
 *         <i>SubformatPatternPart<sub>opt</sub></i>
 *         <i>SubformatPattern</i> <i>SubformatPatternPart</i>
 *         <i>true BooleanPatternPart</i> | <i>false BooleanPatternPart</i>
 *
 * <i>SubFormatPatternPart:</i>
 *         ' <i>QuotedPattern</i> '
 *         <i>UnquotedPattern</i>
 *  
 * <i>BooleanPatternPart:</i> 
 *         <i>UnquotedPattern</i>
 *
 * </pre></blockquote>
 *
 * <p>
 * Within a <i>String</i>, <code>"''"</code> represents a single
 * quote. A <i>QuotedString</i> can contain arbitrary characters
 * except single quotes; the surrounding single quotes are removed.
 * An <i>UnquotedString</i> can contain arbitrary characters
 * except single quotes and left curly brackets. Thus, a string that
 * should result in the formatted message "'{0}'" can be written as
 * <code>"'''{'0}''"</code> or <code>"'''{0}'''"</code>.
 * <p>
 * Within a <i>SubformatPattern</i>, different rules apply.
 * A <i>QuotedPattern</i> can contain arbitrary characters
 * except single quotes; but the surrounding single quotes are
 * <strong>not</strong> removed, so they may be interpreted by the
 * sub-format. For example, <code>"{1,number,$'#',##}"</code> will
 * produce a number format with the pound-sign quoted, with a result
 * such as: "$#31,45".
 * An <i>UnquotedPattern</i> can contain arbitrary characters
 * except single quotes, but curly braces within it must be balanced.
 * For example, <code>"ab {0} de"</code> and <code>"ab '}' de"</code>
 * are valid sub-format patterns, but <code>"ab {0'}' de"</code> and
 * <code>"ab } de"</code> are not.
 * <p>
 * <dl><dt><b>Warning:</b><dd>The rules for using quotes within message
 * format patterns unfortunately have shown to be somewhat confusing.
 * In particular, it isn't always obvious to localizers whether single
 * quotes need to be doubled or not. Make sure to inform localizers about
 * the rules, and tell them (for example, by using comments in resource
 * bundle source files) which strings will be processed by TextFormat.
 * Note that localizers may need to use single quotes in translated
 * strings where the original version doesn't have them.
 * </dl>
 * <p>
 * The <i>ArgumentIndex</i> value is a non-negative integer written
 * using the digits '0' through '9', and represents an index into the
 * <code>arguments</code> array passed to the <code>format</code> methods
 * or the result array returned by the <code>parse</code> methods.
 * <p>
 * The <i>FormatType</i> and <i>FormatStyle</i> values are used to create
 * a <code>Format</code> instance for the format element. The following
 * table shows how the values map to Format instances. Combinations not
 * shown in the table are illegal. A <i>SubformatPattern</i> must
 * be a valid pattern string for the Format subclass used.
 * <p>
 * <table border="1" summary="Shows how FormatType and FormatStyle values map to Format instances"> 
 *   <tr>
 *    <th id="ft">Format Type</th>
 *    <th id="fs">Format Style</th>
 *    <th id="sc">Sub-format Created</th>
 *   </tr><tr>
 *     <td headers="ft"><i>(none)</i></td>
 *     <td headers="fs"><i>(none)</i></td>
 *     <td headers="sc"><code>null</code></td>
 *   </tr><tr>
 *     <td headers="ft" rowspan="3"><code>array</code></td>
 *     <td headers="fs"><i>(none)</i></td>
 *     <td headers="sc"><code>Convert.toCSVString()</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>csv</code></td>
 *     <td headers="sc"><code>Convert.toCSVString()</code></td>
 *   </tr><tr>
 *     <td headers="fs"><i>SubformatPattern</i></td>
 *     <td headers="sc"><code>Convert.toDelimitedString(value,subformatPattern[0]</code></td>
 *   </tr><tr>
 *     <td headers="ft" rowspan="6"><code>date</code></td>
 *     <td headers="fs"><i>(none)</i></td>
 *     <td headers="sc"><code>DateFormat.getDateInstance(DateFormat.DEFAULT,getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>short</code></td>
 *     <td headers="sc"><code>DateFormat.getDateInstance(DateFormat.SHORT,getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>medium</code></td>
 *     <td headers="sc"><code>DateFormat.getDateInstance(DateFormat.DEFAULT,getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>long</code></td>
 *     <td headers="sc"><code>DateFormat.getDateInstance(DateFormat.LONG,getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>full</code></td>
 *     <td headers="sc"><code>DateFormat.getDateInstance(DateFormat.FULL,getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><i>SubformatPattern</i></td>
 *     <td headers="sc"><code>new SimpleDateFormat(subformatPattern,getLocale())</td>
 *   </tr><tr>
 *     <td headers="ft" rowspan="3"><code>list</code></td>
 *     <td headers="fs"><i>(none)</i></td>
 *     <td headers="sc"><code>Convert.toCSVString()</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>csv</code></td>
 *     <td headers="sc"><code>Convert.toCSVString()</code></td>
 *   </tr><tr>
 *     <td headers="fs"><i>SubformatPattern</i></td>
 *     <td headers="sc"><code>Convert.toDelimitedString(value,subformatPattern[0]</code></td>
 *   </tr><tr>
 *     <td headers="ft" rowspan="3"><code>map</code></td>
 *     <td headers="fs"><i>(none)</i></td>
 *     <td headers="sc"><code>Convert.toCSVString()</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>csv</code></td>
 *     <td headers="sc"><code>Convert.toCSVString()</code></td>
 *   </tr><tr>
 *     <td headers="fs"><i>SubformatPattern</i></td>
 *     <td headers="sc"><code>Convert.toDelimitedString(value,subformatPattern[0]</code></td>
 *   </tr><tr>
 *     <td headers="ft" rowspan="5"><code>number</code></td>
 *     <td headers="fs"><i>(none)</i></td>
 *     <td headers="sc"><code>NumberFormat.getInstance(getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>integer</code></td>
 *     <td headers="sc"><code>NumberFormat.getIntegerInstance(getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>currency</code></td>
 *     <td headers="sc"><code>NumberFormat.getCurrencyInstance(getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>percent</code></td>
 *     <td headers="sc"><code>NumberFormat.getPercentInstance(getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>duration</code></td>
 *     <td headers="sc"><code>Misc.duration()</code></td>
 *   </tr><tr>
 *     <td headers="fs"><i>SubformatPattern</i></td>
 *     <td headers="sc"><code>new DecimalFormat(subformatPattern,new DecimalFormatSymbols(getLocale()))</code></td>
 *   </tr><tr>
 *     <td headers="ft" rowspan="6"><code>time</code></td>
 *     <td headers="fs"><i>(none)</i></td>
 *     <td headers="sc"><code>DateFormat.getTimeInstance(DateFormat.DEFAULT,getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>short</code></td>
 *     <td headers="sc"><code>DateFormat.getTimeInstance(DateFormat.SHORT,getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>medium</code></td>
 *     <td headers="sc"><code>DateFormat.getTimeInstance(DateFormat.DEFAULT,getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>long</code></td>
 *     <td headers="sc"><code>DateFormat.getTimeInstance(DateFormat.LONG,getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><code>full</code></td>
 *     <td headers="sc"><code>DateFormat.getTimeInstance(DateFormat.FULL,getLocale())</code></td>
 *   </tr><tr>
 *     <td headers="fs"><i>SubformatPattern</i></td>
 *     <td headers="sc"><code>new SimpleDateFormat(subformatPattern,getLocale())</td>
 *   </tr>
 * </table>
 * <p>
 *
 * <h4>Usage Information</h4>
 *
 * <p>
 * Here are some examples of usage:
 * <blockquote>
 * <pre>
 * String result = TextFormat.format(
 *     "{3,fname} did you notice that at {1,time} on {1,date}, there was {2} on planet {0,number,integer}.",
 *     7,new Date(System.currentTimeMillis()),"a disturbance in the Force");
 *
 * <em>output</em>: Luke did you notice that at 12:30 PM on Jul 3, 2053, there was a disturbance
 *           in the Force on planet 7.
 *
 * </pre>
 * </blockquote>
 * Typically, the message format will come from resources, and the
 * arguments will be dynamically set at runtime.
 *
 * <p>
 * Example 2:
 * <blockquote>
 * <pre>
 * TextFormat form = new TextFormat(
 *     "The disk \"{1}\" contains {0} file(s).");
 *
 * System.out.println(form.format(3,"MyDisk"));
 *
 * // output, with different Arguments
 * <em>output</em>: The disk "MyDisk" contains 3 file(s).
 * <em>output</em>: The disk "MyDisk" contains 1 file(s).
 * <em>output</em>: The disk "MyDisk" contains 1,273 file(s).
 * </pre>
 * </blockquote>
 *
 * <p>
 * When a single argument is parsed more than once in the string, the last match
 * will be the final result of the parsing.  For example,
 * <pre>
 * TextFormat mf = new TextFormat("{0,number,#.##}, {0,number,#.#}");
 * Object[] objs = {new Double(3.1415)};
 * String result = mf.format( objs );
 * // result now equals "3.14, 3.1"
 * objs = null;
 * objs = mf.parse(result, new ParsePosition(0));
 * // objs now equals {new Double(3.1)}
 * </pre>
 * <p>
 * Likewise, parsing with a TextFormat object using patterns containing
 * multiple occurrences of the same argument would return the last match.  For
 * example,
 * <pre>
 * TextFormat mf = new TextFormat("{0}, {0}, {0}");
 * String forParsing = "x, y, z";
 * Object[] objs = mf.parse(forParsing, new ParsePosition(0));
 * // result now equals {new String("z")}
 * </pre>
 *
 * <h4><a name="synchronization">Synchronization</a></h4>
 *
 * <p>
 * Text formats are not synchronized.
 * Creating separate format instances for each thread is recommended.
 * If multiple threads access a format concurrently, it must be synchronized
 * externally.
 *
 * @see          java.util.Locale
 * @see          java.text.Format
 * @see          java.text.NumberFormat
 * @see          java.text.DecimalFormat
 * @author       Chris Pratt
 */
public class TextFormat {

  /**
   * TextNode
   */
  static class TextNode extends TextFormatter {
    private String text;

    /**
     * Constructor
     *
     * @param text The Static Text for this node
     */
    public TextNode (CharSequence text) {
      super(null,null,null);
      this.text = text.toString();
    } //TextNode

    /**
     * Return the static String
     *
     * @param args The Formatting Arguments (ignored)
     * @return The Static Text
     */
    @Override
    public String toString (Object... args) {
      return text;
    } //toString

  } //*TextNode

  /**
   * StringNode
   */
  static class StringNode extends TextFormatter {

    /**
     * Constructor
     *  
     * @param parent The parent TextFormat 
     * @param key The Argument Index or Map Key for this node 
     * @param bean The additional Bean reference String 
     */
    public StringNode (TextFormat parent,CharSequence key,CharSequence bean) {
      super(parent,key,bean);
    } //StringNode

    /**
     * Return the String representation of the Argument
     *
     * @param args The Formatting Arguments
     * @return The String Representation of the proper argument
     */
    @Override
    public String toString (Object... args) {
      Object o = getValue(args);
      return (o != null) ? o.toString() : null;
    } //toString

  } //*StringNode

  private static final PrototypeMap<String,TextFormatter> DEF_FORMATTERS;
  static {
    DEF_FORMATTERS = new PrototypeMap<>();
    DEF_FORMATTERS.put("array",CollectionFormatter.class);
    DEF_FORMATTERS.put("binary",BinaryFormatter.class);
    DEF_FORMATTERS.put("boolean",BooleanFormatter.class);
    DEF_FORMATTERS.put("choice",ChoiceFormatter.class);
    DEF_FORMATTERS.put("date",DateFormatter.class);
    DEF_FORMATTERS.put("list",CollectionFormatter.class);
    DEF_FORMATTERS.put("map",CollectionFormatter.class);
    DEF_FORMATTERS.put("number",NumberFormatter.class);
    DEF_FORMATTERS.put("string",StringFormatter.class);
    DEF_FORMATTERS.put("substring",SubstringFormatter.class);
    DEF_FORMATTERS.put("time",TimeFormatter.class);
  }
  private static final Class<?>[] ARGS3 = {TextFormat.class,CharSequence.class,CharSequence.class};
  private static final Class<?>[] ARGS4 = {TextFormat.class,CharSequence.class,CharSequence.class,CharSequence.class};
  private String text;
  private TextFormatter base = null;
  private PrototypeMap<String,TextFormatter> formatters;

    /**
     * Constructor
     *
     * @param text The Text to be Formatted
     */
  public TextFormat (String text) {
    if(text != null) {
      this.text = text;
      formatters = new PrototypeMap<>(DEF_FORMATTERS);
    } else {
      throw new NullPointerException("Text Format Pattern cannot be null");
    }
  } //TextFormat

  /**
   * Constructor
   *
   * @param text The Text Format Pattern 
   * @param formatters The Prototype Map of Text Formatters 
   */
  private TextFormat (String text,PrototypeMap<String,TextFormatter> formatters) {
    if(text != null) {
      this.text = text;
      this.formatters = formatters;
    } else {
      throw new NullPointerException("Text Format Pattern cannot be null");
    }
  } //TextFormat

  /**
   * Compile the supplied format string into an executable stack
   */
  private void compile () {
    int nd,pos,slf4jndx = 0;
    TextFormatter curr,prev = null;
    StringBuilder buf = new StringBuilder();
    char[] ch = text.toCharArray();
    for(int i = 0;i < ch.length;i++) {
      switch(ch[i]) {
        case '\'':
          if(ch[++i] == '\'') {
            buf.append('\'');
          } else {
            while((i < ch.length) && (ch[i] != '\'')) {
              buf.append(ch[i++]);
            }
          }
          break;
        case '{':
          if(buf.length() > 0) {
            curr = new TextNode(buf);
            if(base == null) {
              base = prev = curr;
            } else {
              prev.setNext(curr);
              prev = curr;
            }
            buf.setLength(0);
          }
          if(ch[i + 1] != '}') {
            StringBuilder key = new StringBuilder();
            while((ch[++i] != '.') && (ch[i] != ',') && (ch[i] != '}')) {
              key.append(ch[i]);
            }
            StringBuilder bean = null;
            if(ch[i] == '.') {
              bean = new StringBuilder();
              if(Character.isJavaIdentifierStart(ch[++i])) {
                do {
                  bean.append(ch[i]);
                } while(Character.isJavaIdentifierPart(ch[++i]) || (ch[i] == '.') || (ch[i] == ':'));
              }
            }
            if(ch[i] == ',') {
              nd = Misc.arrayIndexOf(ch,'}',++i);
              if(((pos = Misc.arrayIndexOf(ch,',',i)) != -1) && (pos < nd)) {
                int nest = 0;
                boolean literal = false;
                nd = pos + 1;
                while((ch[nd] != '}') || literal || (nest > 0)) {
                  switch(ch[nd]) {
                    case '\'':
                      literal = !literal;
                      break;
                    case '{':
                      if(!literal) {
                        ++nest;
                      }
                      break;
                    case '}':
                      if(!literal) {
                        --nest;
                      }
                      break;
                  }
                  ++nd;
                }
                curr = formatters.newInstance(new String(ch,i,pos - i),ARGS4,new Object[] {this,key,bean,new String(ch,++pos,nd - pos)});
              } else {
                curr = formatters.newInstance(new String(ch,i,nd - i),ARGS3,new Object[] {this,key,bean});
              }
              if(curr == null) {
                curr = new StringNode(this,key,bean);
              }
              i = nd;
            } else {
              curr = new StringNode(this,key,bean);
            }
            key.setLength(0);
          } else {
              // Emulate SLF4j {} style behavior
            curr = new StringNode(this,String.valueOf(slf4jndx++),null);
          }
          if(base == null) {
            base = prev = curr;
          } else {
            prev.setNext(curr);
            prev = curr;
          }
          while((i < ch.length) && (ch[i] != '}')) {
            ++i;
          }
          break;
        default:
          buf.append(ch[i]);
          break;
      }
    }
    if(buf.length() > 0) {
      curr = new TextNode(buf);
      if(base == null) {
        base = curr;
      } else {
        prev.setNext(curr);
      }
    }
  } //compile

  /**
   * Format the String using the supplied Arguments
   *
   * @param args The Format Arguments
   * @return The Formatted String
   */
  public String format (Object... args) {
    if(base == null) {
      compile();
    }
    StringBuilder buf = new StringBuilder();
    TextFormatter curr = base;
    while(curr != null) {
      buf.append(curr.toString(args));
      curr = curr.getNext();
    }
    return buf.toString();
  } //format

  /**
   * Return a new Text Formatter using the same set of Formatters as this 
   * instance. 
   *
   * @param text The new Text to be formatted
   * @return The new Text Format
   */
  public TextFormat subformat (String text) {
    return new TextFormat(text,formatters);
  } //subformat

  /**
   * Add a new Formatter to this TextFormat instance
   *
   * @param key The key to recognize this TextFormatter by 
   * @param formatter The Class of the TextFormatter to register
   */
  public void addFormatter (String key,Class<? extends TextFormatter> formatter) {
    formatters.put(key,formatter);
    base = null;
  } //addFormatter

  /**
   * Convenience Static Formatter
   *
   * @param text The Format Pattern
   * @param args The Format Arguments
   * @return The Formatted String
   */
  public static String format (String text,Object... args) {
    return new TextFormat(text).format(args);
  } //format
  
  /**
   * Send the formatted output to the System.out Standard Output Stream
   *
   * @param text The Format Pattern
   * @param args The Format Arguments
   */
  public static void stdout (String text,Object... args) {
    System.out.println(new TextFormat(text).format(args));
  } //stdout
  
  /**
   * Send the formatted output to the System.err Standard Error Stream
   *
   * @param text The Format Pattern
   * @param args The Format Arguments
   */
  public static void stderr (String text,Object... args) {
    System.err.println(new TextFormat(text).format(args));
  } //stderr
  
  /**
   * Add a new Formatter to all future instances of TextFormat
   *
   * @param key The key to recognize this TextFormatter by
   * @param formatter The Class of the TextFormatter to register
   */
  public static void addDefaultFormatter (String key,Class<? extends TextFormatter> formatter) {
    DEF_FORMATTERS.put(key,formatter);
  } //addDefaultFormatter

} //*TextFormat
